FINERACT-2688: Fix loan disbursement for checker-only users via maker-checker permission check - #6128
Conversation
30e1794 to
f830f34
Compare
Aman-Mittal
left a comment
There was a problem hiding this comment.
Need a integration test for this change before merge
Which explain whole process for this fix for verification
Enable maker-checker for loan disbursement
Maker submits → queued as AWAITING_APPROVAL
Maker submits again → HTTP 403 error.msg.maker.checker.duplicate.pending.submission
Checker-only user (has DISBURSE_LOAN_CHECKER) submits → auto-approves and disburses
Checker-only with no pending entry → HTTP 403 error.msg.maker.checker.checker.only.cannot.initiate
Existing /makerchecker/{id} approve/reject flow unaffected
| this.context.authenticatedUser(wrapper).validateHasPermissionTo(wrapper.getTaskPermissionName()); | ||
| final String taskPermission = wrapper.getTaskPermissionName(); | ||
| final boolean hasBasePermission = !currentUser.hasNotPermissionForAnyOf(taskPermission); | ||
| final boolean hasCheckerPermission = !currentUser.hasNotPermissionForAnyOf("CHECKER_SUPER_USER", taskPermission + "_CHECKER"); |
There was a problem hiding this comment.
This Logic can be simplified more
|
@aya-abdallah-FOO also please amend your commit to exclude AI co-author |
b71128f to
62ec993
Compare
|
@aya-abdallah-FOO Also please sign your commits |
b0bd55d to
89d50b9
Compare
@Aman-Mittal can you please check if all is good? |
yes now properly signed |
89d50b9 to
106bac4
Compare
|
@Aman-Mittal can you please run the CI and if you know why would any of them still fail? |
|
CI Results — Shard 6 passed; Shard 4 and Kafka failures explained below Shard 6 (our test shard) - Passed on MySQL, MariaDB, PostgreSQL LoanDisbursementMakerCheckerIntegrationTest lands in Shard 6 of 15 (confirmed via the round-robin sort order). All three DB variants passed on the latest commit (b1fc327). Shard 4 failures (MySQL, MariaDB, PostgreSQL) — test-suite reshuffling, not a logic bug Adding LoanDisbursementMakerCheckerIntegrationTest inserts a new class alphabetically between LoanDisbursalDateValidationTest and LoanDisbursementDetailsIntegrationTest, shifting the round-robin shard assignment of every test class that sorts after it. As a result, tests that were previously in Shard 3 on develop (e.g. LoanMultipleDisbursementRepaymentScheduleTest, UndoRepaymentWithDownPaymentIntegrationTest) now run in Shard 4 after the original Shard-4 tests, creating a new execution ordering. Our production code changes are guarded behind the global MAKER_CHECKER configuration (defaults to false/disabled), so the new logCommandSource paths and CommandSourceRepository query are never exercised in any of the Shard 4 tests. The failures are not in our code's execution path. One specific pre-existing fragility we found: UndoRepaymentWithDownPaymentIntegrationTest enables ENABLE_BUSINESS_DATE at the top of its @test method without a try/finally — if the test fails mid-way, the config leaks into subsequent tests. This issue exists on develop too but didn't surface before because the test ran in Shard 3 with different predecessors. Could a maintainer check the artifact test-results-*-shard-4-attempt-2 to confirm which specific test is the first to fail? That would let us pinpoint the root cause and file a targeted fix for those tests. Kafka Smoke Test — transient Docker infrastructure failure The failure is in step 8 "Build the image" (jibDockerBuild), before any Kafka tests run. This is a Docker daemon / image-pull issue in the CI runner, unrelated to our Java changes (Shard 6 compilation and startup succeeded). Could a maintainer re-run just the Kafka smoke test job? @Aman-Mittal |
|
@aya-abdallah-FOO Please review the failing tests. |
@adamsaghy FYI |
|
@aya-abdallah-FOO please resolve conflicts and rebase to latest develop. |
…-checker permission check When maker-checker is enabled, a user holding only the <ACTION>_CHECKER permission (e.g. DISBURSE_LOAN_CHECKER) was rejected with a permission error instead of being routed to the pending approval queue. - logCommandSource(): detect checker-only users, look up the pending AWAITING_APPROVAL command for the same action/entity/resource, and auto-approve it; block makers from duplicate pending submissions - validateMakerChecker(): broaden approval condition from isCheckerSuperUser() to also accept role-specific _CHECKER permissions - Add CommandSourceRepository.findPendingByActionAndEntityAndResource() native query (searches all resource-id columns) - Add MakerCheckerCheckerOnlyInitiationException and MakerCheckerDuplicatePendingSubmissionException Signed-off-by: aya.abdallah <aya.abdallah@foo.mobi>
…ix MakercheckerTest The previous change replaced user.isCheckerSuperUser() with a broader userHasCheckerPermission check that also matched task-specific _CHECKER permissions. This caused makers who hold both base and _CHECKER permissions to have their commands auto-approved instead of routed to AWAITING_APPROVAL, breaking MakercheckerTest across all three databases. The checker-only flow introduced by FINERACT-2688 is handled upstream in logCommandSource, so validateMakerChecker never sees checker-only users and needs no change. Signed-off-by: aya.abdallah <aya.abdallah@foo.mobi>
…t maker-checker flow
Covers all five scenarios:
- Maker submits disbursement → queued as AWAITING_APPROVAL
- Maker submits again → HTTP 403 duplicate pending submission
- Checker-only user with no pending entry → HTTP 403 cannot initiate
- Checker-only user auto-approves pending entry and disburses
- Traditional /makerchecker/{id}?command=approve flow unaffected
Signed-off-by: aya.abdallah <aya.abdallah@foo.mobi>
Two fixes for the flaky CI failures in MySQL/MariaDB shards: 1. Move global config updates (MAKER_CHECKER, ENABLE_SAME_MAKER_CHECKER) inside the try block so the finally cleanup is guaranteed even if the second update throws — preventing MAKER_CHECKER=true from leaking to subsequent tests in the same shard and causing unexpected maker-checker queuing. 2. Narrow findPendingByActionAndEntityAndResource to match only resource_id, loan_id, and savings_account_id. The previous query also matched client_id, office_id, group_id, product_id, and other contextual columns. When a loan ID coincidentally equalled a contextual ID stored on another pending entry, the wrong command was returned — causing Scenario 3 (checker-only no-entry → 403) to auto-approve the wrong loan instead of throwing, breaking the assertion.
d7fc924 to
cd65b81
Compare
| where upper(c.action_name) = upper(?1) | ||
| and upper(c.entity_name) = upper(?2) | ||
| and ( | ||
| c.resource_id = ?3 |
There was a problem hiding this comment.
why ordinal? i think param name like "entityName" would make more sense for readability.
LoanNextPaymentDueAmountIntegrationTest enabled three business events (LoanApprovedBusinessEvent, LoanBalanceChangedBusinessEvent, LoanDisbursalBusinessEvent) in each test method but never disabled them. Adding LoanDisbursementMakerCheckerIntegrationTest alphabetically before it caused it to shift shards, exposing the leaked event state to subsequent tests and causing intermittent CI failures. Add @AfterEach cleanup to reliably disable the events after each method. Also convert the native SQL @query to JPQL so it works identically on MariaDB, MySQL, and PostgreSQL without dialect-specific column names.
…ection Replace the native SQL query (SELECT DISTINCT c.* with nativeQuery=true) with a plain JPQL scalar query that returns only the command source IDs. The native SQL had two failure modes: 1. MySQL/MariaDB: SELECT DISTINCT c.* triggers internal key creation for the TEXT column command_as_json, which MySQL cannot index without an explicit prefix length — resulting in a runtime SQL error. 2. The previous JPQL used DISTINCT on the entity type, causing Hibernate to JOIN-fetch eager @manytoone relationships and then apply DISTINCT over those columns, hitting the same TEXT-column restriction on MariaDB. Returning only c.id avoids both issues: no TEXT columns, no JOIN, no DISTINCT. The callers only needed the ID anyway (they reload the full entity via findById in approveEntry/validateMakerCheckerTransaction).
Summary
Fixes a bug where a user holding only a role-specific
_CHECKERpermission (e.g.DISBURSE_LOAN_CHECKER) was rejected with a permission error instead of being routed to thepending maker-checker approval queue.
Root causes:
PortfolioCommandSourceWritePlatformServiceImpl.logCommandSource()calledvalidateHasPermissionTo(taskPermission)unconditionally, rejecting checker-only usersCommandSourceService.validateMakerChecker()only marked a command as checked forCHECKER_SUPER_USER, ignoring role-specific_CHECKERpermissionsChanges:
logCommandSource(): detect checker-only users (has_CHECKERbut not base permission), auto-approve the pending AWAITING_APPROVAL entry; block makers from duplicate pendingsubmissions
validateMakerChecker(): replaceisCheckerSuperUser()withhasNotPermissionForAnyOf("CHECKER_SUPER_USER", permission + "_CHECKER")CommandSourceRepository.findPendingByActionAndEntityAndResource()native SQL queryMakerCheckerCheckerOnlyInitiationExceptionandMakerCheckerDuplicatePendingSubmissionException(both → HTTP 403)JIRA
https://issues.apache.org/jira/browse/FINERACT-2688
Test plan
error.msg.maker.checker.duplicate.pending.submissionDISBURSE_LOAN_CHECKER) submits → auto-approves and disburseserror.msg.maker.checker.checker.only.cannot.initiate/makerchecker/{id}approve/reject flow unaffected